<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8" />
    <meta name="author" content="iveseenthedark" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#333333" />
    <link rel="apple-touch-icon" sizes="180x180" href="https://img.hellogithub.com/favicon/apple-touch-icon.png">
    <link rel="android-chrome" sizes="192x192" href="https://img.hellogithub.com/favicon/android-chrome-192x192.png">
    <link rel="android-chrome" sizes="512x512" href="https://img.hellogithub.com/favicon/android-chrome-512x512.png">
    <link rel="icon" type="image/png" sizes="32x32" href="https://img.hellogithub.com/favicon/favicon-32x32.png">
    <link rel="icon" type="image/png" sizes="16x16" href="https://img.hellogithub.com/favicon/favicon-16x16.png">
    <link rel="icon" href="https://img.hellogithub.com/favicon/favicon.ico">

    <title>A S C I I C A M</title>
    <style>
        #ascii-cam pre,
        body,
        html {
            width: 100vw;
            height: 100vh;
            margin: 0;
        }

        * {
            box-sizing: border-box;
        }

        body {
            overflow: hidden;
            background-color: #000;
            color: #eee;
            font-family: monospace;
        }

        #logo,
        #enable-cam-msg {
            transform: translate(-50%, -50%);
            position: absolute;
            left: 50%;
            top: 50%;
        }

        .fade {
            transition: opacity 0.25s ease-in-out;
        }

        #logo {
            line-height: 1.5;
        }

        #ascii-cam {
            opacity: 0;
        }

        #ascii-cam pre {
            position: absolute;
            top: 0;
            left: 0;
            overflow: hidden;
            line-height: 0.6;
            user-select: none;
            mix-blend-mode: screen;
            font-size: 1.2em;
            font-size: 2vh;
        }

        @media (max-height: 50em) {
            #ascii-cam pre {
                font-size: 1em;
            }
        }

        @media (min-height: 60em) {
            #ascii-cam pre {
                font-size: 1.2em;
            }
        }

        #ascii-cam #r-output {
            color: red;
        }

        #ascii-cam #g-output {
            color: #0f0;
        }

        #ascii-cam #b-output {
            color: #00f;
        }

        #enable-cam-msg {
            opacity: 0;
            font-size: 3rem;
            background-color: white;
            color: black;
            margin: 0;
            max-width: 50vw;
            white-space: pre-line;
            text-align: center;
            font-weight: 900;
        }

        #canvas,
        #video {
            display: none;
        }
    </style>
</head>

<body>
    <pre id="logo" class="fade">

   _n_|_|_,_
  |===.-.===|
  |  ((_))  |
  '==='-'==='
A S C I I C A M
点击“允许”使用摄像头
    </pre>

    <pre id="enable-cam-msg" class="fade">Allow access to camera to use</pre>

    <div id="ascii-cam" class="fade">
        <div class="layers">
            <pre id="r-output"></pre>
            <pre id="g-output"></pre>
            <pre id="b-output"></pre>
        </div>
    </div>

    <video id="video" autoplay="true"></video>
    <canvas id="canvas" width="640" height="480"></canvas>
    <script type="application/javascript">
        (() => {
            'use strict';

            let interval;
            let r_layer, g_layer, b_layer;
            const palette = [' ', '.', '-', ':', '+', '*', '=', '%', '@', '#'];
            const vanity = 'B Y  I V E S E E N T H E D A R K';

            /**
             * @param {*} stream
             */
            const handle_video = stream => {
                const canvas = document.getElementById('canvas');
                const ctx = canvas.getContext('2d');

                const video = document.getElementById('video');
                // Older browsers may not have srcObject
                if ('srcObject' in video) {
                    video.srcObject = stream;
                } else {
                    // Avoid using this in new browsers, as it is going away.
                    video.src = window.URL.createObjectURL(stream);
                }
                video.addEventListener('loadedmetadata', () => {
                    window.addEventListener('resize', () => set_canvas_dimensions(video, canvas, ctx));
                    set_canvas_dimensions(video, canvas, ctx);

                    // Hide Logo
                    play_video(video, canvas, ctx);
                    setTimeout(() => {
                        document.getElementById('logo').style.opacity = 0;
                        document.getElementById('ascii-cam').style.opacity = 1;
                    }, 1000);
                });
            };

            /**
             */
            const handle_no_video = () => {
                setTimeout(() => {
                    document.getElementById('logo').style.opacity = 0;
                    document.getElementById('ascii-cam').style.opacity = 1;
                    document.getElementById('enable-cam-msg').style.opacity = 1;
                    play_random();
                }, 1000);
            };

            /**
             * @param {*} e
             */
            const handle_error = e => {
                console.error('Error', e);
                handle_no_video();
            };

            /**
             * The greyscale value is calculated GREY = 0.299 * RED + 0.587 * GREEN + 0.114 * BLUE
             * For more information see http://en.wikipedia.org/wiki/Grayscale
             * @param {Array} image_data
             */
            const fade_to_grey = pixels => pixels.map(pixel => pixel[0] * 0.299 + pixel[1] * 0.587 + pixel[2] * 0.114);

            /**
             */
            const play_video = (video, canvas, ctx) => {
                clearInterval(interval);

                // Buffer reverse
                ctx.drawImage(video, 0, 0, canvas.width * -1, canvas.height);

                // Grab view
                const dx = canvas.dataset.hoffset || 0;
                const dy = canvas.dataset.voffset || 0;
                const image_width = canvas.width - dx * 2;
                const image_data = ctx.getImageData(dx, dy, image_width, canvas.height - dy * 2).data;

                // Chunk
                const channels = 4;
                const pixels = Array.from(Array(Math.ceil(image_data.length / channels)), (_, i) =>
                    image_data.slice(i * channels, i * channels + channels)
                );

                // Draw
                let r_output = '';
                let g_output = '';
                let b_output = '';
                pixels.forEach((pixel, i) => {
                    // Break for new line
                    if (i && i % image_width === 0) {
                        r_output += '\n';
                        g_output += '\n';
                        b_output += '\n';
                    }

                    const b_idx = Math.floor((pixel[2] / 255.0) * (palette.length - 1));
                    const r_idx = Math.floor((pixel[0] / 255.0) * (palette.length - 1));
                    const g_idx = Math.floor((pixel[1] / 255.0) * (palette.length - 1));

                    if (i && i >= image_width - vanity.length && i < image_width) {
                        // Input vanity message
                        r_output += vanity[vanity.length + i - image_width];
                        g_output += vanity[vanity.length + i - image_width];
                        b_output += palette[g_idx];
                    } else {
                        // Output camera data
                        r_output += palette[r_idx];
                        g_output += palette[g_idx];
                        b_output += palette[b_idx];
                    }
                }, this);

                r_layer.textContent = r_output;
                g_layer.textContent = g_output;
                b_layer.textContent = b_output;

                interval = setInterval(() => play_video(video, canvas, ctx), 33);
            };

            /**
             */
            const play_random = () => {
                clearInterval(interval);

                const char_dims = get_character_dimensions();
                const screen_dims = get_screen_dimensions(char_dims);
                const total_chars = Math.floor(screen_dims.char_width * screen_dims.char_height);

                const mid_x = Math.floor(screen_dims.char_width / 2);
                const mid_y = Math.floor(screen_dims.char_height / 2);

                let radius_sq = mid_x * mid_x + mid_y + mid_y;
                radius_sq = radius_sq < 2500 ? 2500 : radius_sq; // Stop the circle getting too small

                let r_output = '',
                    g_output = '',
                    b_output = '';
                for (let i = 0; i < total_chars; i++) {
                    // Break for new line
                    if (i && i % screen_dims.char_width === 0) {
                        r_output += '\n';
                        g_output += '\n';
                        b_output += '\n';
                    }

                    const char_pos_x = i % screen_dims.char_width;
                    const char_pos_y = i / screen_dims.char_width;

                    let dist = (char_pos_x - mid_x) * (char_pos_x - mid_x) + (char_pos_y - mid_y) * (char_pos_y - mid_y);
                    dist = dist > radius_sq ? radius_sq : dist; // Distance can be further than radius, clamp

                    const range = Math.floor(palette.length * (1 - dist / radius_sq));
                    const r_idx = Math.floor(Math.random() * range);
                    const g_idx = Math.floor(Math.random() * range);
                    const b_idx = Math.floor(Math.random() * range);

                    r_output += palette[r_idx];
                    g_output += palette[g_idx];
                    b_output += palette[b_idx];
                }

                r_layer.textContent = r_output;
                g_layer.textContent = g_output;
                b_layer.textContent = b_output;

                interval = setInterval(play_random, 100);
            };

            /**
             */
            const get_character_dimensions = () => {
                const span = document.createElement('span');
                span.textContent = 'X';
                span.style.position = 'absolute';
                span.style.left = '-100px';

                document.querySelector('#ascii-cam pre').appendChild(span);
                const dimensions = span.getBoundingClientRect();
                span.remove();

                return dimensions;
            };

            /**
             */
            const get_screen_dimensions = char_dims => {
                return {
                    width: window.innerWidth,
                    height: window.innerHeight,
                    char_width: Math.ceil(window.innerWidth / char_dims.width),
                    char_height: Math.ceil(window.innerHeight / char_dims.height)
                };
            };

            /**
             */
            const set_canvas_dimensions = (video, canvas, ctx) => {
                console.log('setting canvas dimensions');
                const char_dims = get_character_dimensions();
                const screen_dims = get_screen_dimensions(char_dims);

                let width = Math.floor(screen_dims.width / char_dims.width);
                let height = Math.max(width * 0.75, screen_dims.char_height);

                if (height > screen_dims.char_height) {
                    canvas.dataset.voffset = Math.floor((height - screen_dims.char_height) / 2);
                    canvas.dataset.hoffset = 0;
                } else {
                    width = (height / 3) * 4;
                    canvas.dataset.hoffset = Math.floor((width - screen_dims.char_width) / 2);
                    canvas.dataset.voffset = 0;
                }

                video.width = canvas.width = width;
                video.height = canvas.height = height;

                // Reset scale if context exists
                ctx && ctx.scale(-1, 1);
            };

            // Onload
            window.addEventListener('load', () => {
                if (navigator && navigator.mediaDevices) {
                    // Grab layers
                    r_layer = document.getElementById('r-output');
                    g_layer = document.getElementById('g-output');
                    b_layer = document.getElementById('b-output');

                    const constraints = { audio: false, video: {} };
                    navigator.mediaDevices
                        .getUserMedia(constraints)
                        .then(handle_video)
                        .catch(handle_error);
                } else {
                    handle_no_video();
                }
            });
        })();
    </script>
</body>

</html>